﻿using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;

namespace Heimdallr.Net
{
    /// <summary>
    /// Represents a asynchronized HTTP multipart streaming client.
    /// </summary>
    /// <remarks>
    /// For additional information about multipart, see RFC1341 - MIME (Multipurpose Internet Mail Extensions) section 7.2.
    /// </remarks>
    public class HttpMultipartTriggerElement : ServiceElement, ITriggerElement
    {
        #region Field

        internal readonly static string BoundaryPrefix = "boundary=";

        private readonly Uri uri;
        private readonly ICredentials credentials;

        private int maxReceivedSize;
        private HttpMultipartSplitter splitter;
        private HttpWebResponse response;

        #endregion

        #region Property

        /// <summary>
        /// Gets the specified URL of this instance.
        /// </summary>
        public Uri RequestUri
        {
            get { return this.uri; }
        }

        /// <summary>
        /// Gets the specified credentials used by this instance.
        /// </summary>
        public ICredentials Credentials
        {
            get { return this.credentials; }
        }

        /// <summary>
        /// Gets or sets the maximum data length of notifications.
        /// A negative value means no upper bound for the data length of incoming notifications.
        /// </summary>
        /// <exception cref="NotSupportedException">The element does not support this property.</exception>
        /// <remarks>
        /// If the value of this property is greater than -1 and <see cref="Notification.ContentLength">ContentLength</see> is above it,
        /// the element will ignore it and will not raise the <see cref="Notify"/> event.
        /// </remarks>
        public virtual int MaxReceivedSize
        {
            get { return this.maxReceivedSize; }
            set { this.maxReceivedSize = value; }
        }

        #endregion

        #region Event

        /// <summary>
        /// Occurs when a notification needs to published by <see cref="INotifyElement"/>. 
        /// </summary>
        public event FireNotificationEventHandler Notify;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance with specified URL.
        /// </summary>
        /// <param name="uri">Specified URL.</param>
        /// <exception cref="ArgumentNullException"><paramref name="uri"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="uri"/> is not an HTTP or HTTPS URL.</exception>
        public HttpMultipartTriggerElement(Uri uri)
            : this(uri, null)
        {
        }


        /// <summary>
        /// Initializes a new instance with specified URL and credentials.
        /// </summary>
        /// <param name="uri">Specified URL.</param>
        /// <param name="credentials">Specified credentials.</param>
        /// <exception cref="ArgumentNullException"><paramref name="uri"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="uri"/> is not an HTTP or HTTPS URL.</exception>
        public HttpMultipartTriggerElement(Uri uri, ICredentials credentials)
        {
            if (uri == null)
                throw new ArgumentNullException("uri");

            if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
                throw new ArgumentException("Not an HTTP or HTTPS URL.", "uri");

            this.uri = uri;
            this.credentials = credentials;
            this.maxReceivedSize = int.MaxValue;
        }

        #endregion

        #region ServiceElement Implementation Member

        /// <summary>
        /// Executes when the status of element is changing from <see cref="ServiceElementStatus">Stopped</see> to <see cref="ServiceElementStatus">Running</see>.
        /// </summary>
        protected override void OnStart()
        {
            this.splitter = new HttpMultipartSplitter();
            this.splitter.PartReceived += this.splitter_PartReceived;
            this.splitter.Error += this.splitter_Error;
            this.splitter.Completed += this.splitter_Completed;
            this.splitter.Start(this.CreateWorkingItem());
        }

        /// <summary>
        /// Executes when the status of element is changing from <see cref="ServiceElementStatus">Running</see> to <see cref="ServiceElementStatus">Stopped</see>.
        /// </summary>
        protected override void OnStop()
        {
            if (this.splitter == null)
                return;

            this.splitter.Stop();
            this.splitter.PartReceived -= this.splitter_PartReceived;
            this.splitter.Error -= this.splitter_Error;
            this.splitter.Completed -= this.splitter_Completed;
        }

        #endregion

        #region HttpMultipartSplitter EventHandler

        private void splitter_PartReceived(object sender, PartReceivedEventArgs e)
        {
            this.OnNotify(new FireNotificationEventArgs(this.CreateNotification(NotificationLevel.Info, e.Body, this.uri, e.Headers)));
        }

        private void splitter_Error(object sender, ExceptionsEventArgs e)
        {
            HttpMultipartSplitter splitter = sender as HttpMultipartSplitter;

            splitter.Dispose();

            if (this.response != null)
                this.response.Close();

            base.OnError(e);
        }

        private void splitter_Completed(object sender, CompletedEventArgs e)
        {
            HttpMultipartSplitter splitter = sender as HttpMultipartSplitter; 

            splitter.Dispose();

            if (this.response != null)
                this.response.Close();

            base.Status = ServiceElementStatus.Stopped;
        }

        #endregion

        #region Protected Method

        /// <summary>
        /// When element starts, creates a new <see cref="HttpWebRequest"/> with current URI and credentials used by this instance.
        /// </summary>
        /// <param name="uri">Current URI of this instance.</param>
        /// <param name="credentials">Current credentials of this instance.</param>
        /// <returns>A new request instance.</returns>
        protected virtual HttpWebRequest CreateWebRequest(Uri uri, ICredentials credentials)
        {
            return CreateDefaultWebRequest(uri, credentials);
        }

        /// <summary>
        /// When element receives latest part from server, creates a new <see cref="HttpNotification"/> instance. 
        /// </summary>
        /// <param name="level">The level of this notification.</param>
        /// <param name="data">The data of this notification.</param>
        /// <param name="uri">The original URL of this notification.</param>
        /// <param name="headers">The headers of this notification.</param>
        /// <returns>A new notification instance.</returns>
        protected virtual HttpNotification CreateNotification(NotificationLevel level, byte[] data, Uri uri, NameValueCollection headers)
        {
            return new HttpNotification(NotificationLevel.Info, data, this.uri, headers);
        }

        #endregion

        #region Other

        private HttpMultipartWorkingItem CreateWorkingItem()
        {
            HttpWebRequest request = CreateWebRequest(this.uri, this.credentials);

            if (request == null || request.RequestUri != this.uri)
                request = CreateDefaultWebRequest(this.uri, this.credentials);

            HttpWebResponse response = request.GetResponse() as HttpWebResponse;
            string contentType = response.ContentType;
            int boundaryPrefixIndex = contentType.IndexOf(BoundaryPrefix);

            // It it's not a multipart response.
            if (boundaryPrefixIndex < 0)
            {
                long contentLength = response.ContentLength;

                using (response)
                    if (contentLength > 0 && contentLength < this.maxReceivedSize)
                    {
                        using (Stream rs = response.GetResponseStream())
                        using (MemoryStream ms = contentLength < 0 ? new MemoryStream() : new MemoryStream((int)contentLength))
                        {
                            rs.CopyTo(ms);
                            this.OnNotify(new FireNotificationEventArgs(this.CreateNotification(NotificationLevel.Info, ms.ToArray(), this.uri, response.Headers)));
                        }
                    }

                base.Status = ServiceElementStatus.Stopped;
                return null;
            }

            /*
             * 0---------1---------2---------3---------4---------5
             *      multipart/x-mixed-replace; boundary=--boundary
             */

            int semicolonIndex = contentType.IndexOf(';', boundaryPrefixIndex);
            int boundaryIndex = boundaryPrefixIndex + BoundaryPrefix.Length;
            string boundary;

            if (semicolonIndex < 0)
                boundary = contentType.Substring(boundaryIndex);
            else
                boundary = contentType.Substring(boundaryIndex, semicolonIndex - boundaryIndex);

            this.response = response;

            return new HttpMultipartWorkingItem(boundary, response.GetResponseStream(), this.maxReceivedSize);
        }

        private HttpWebRequest CreateDefaultWebRequest(Uri uri, ICredentials credentials)
        {
            if (uri == null)
                throw new ArgumentNullException("uri");

            HttpWebRequest request = HttpWebRequest.Create(uri) as HttpWebRequest;

            request.Credentials = credentials;
            request.KeepAlive = true;

            return request;
        }

        #endregion

        #region Raise Event

        /// <summary>
        /// When element receives latest part from server, raises the <see cref="Notify"/> event.
        /// </summary>
        /// <param name="e">Event data for the event.</param>
        /// <exception cref="ArgumentNullException">Parameter <paramref name="e"/> is null.</exception>
        protected virtual void OnNotify(FireNotificationEventArgs e)
        {
            if (e == null)
                throw new ArgumentNullException("e");

            if (this.Notify != null)
                this.Notify.BeginInvoke(this, e, this.Notify.EndInvoke, null);
        }

        #endregion
    }
}